#include "cybertron/network/SocketUdpClient.hpp"
#include "cybertron/core/UtilCRT.hpp"
#include "cybertron/core/UtilCRC.hpp"
#include <thread>
#include <asio.hpp>
#include "b64/encode.h"
#include "b64/decode.h"
#include "cybertron/network/perf.hpp"

CYBERTRON_BEGIN

class SocketUdpClientImpl
{
public:
	SocketUdpSenderConfig mConfig;
	std::shared_ptr<asio::ip::udp::socket> mpSocket;
	std::shared_ptr<asio::io_service> mpIoService;
	std::shared_ptr<bool> mThreadRunning;
	std::shared_ptr<std::thread> mpSenderThread;
};

CybertronCoreAPI SocketUdpClient::SocketUdpClient()
{
	mpImpl = new SocketUdpClientImpl;
}

CybertronCoreAPI SocketUdpClient::~SocketUdpClient()
{
	delete mpImpl;
	mpImpl = nullptr;
}

CybertronCoreAPI bool SocketUdpClient::initialize(const SocketUdpSenderConfig& config)
{
	clear();

	mpImpl->mConfig = config;
	mpImpl->mThreadRunning = std::make_shared<bool>(true);
	mpImpl->mpIoService = std::make_shared<asio::io_service>();
	mpImpl->mpSocket = std::make_shared<asio::ip::udp::socket>(
		*(mpImpl->mpIoService),
		asio::ip::udp::endpoint(asio::ip::udp::v4(), 0));

	if (config.isBroadcast)
	{
		mpImpl->mpSocket->set_option(asio::socket_base::broadcast(true));
	}

	// start background thread
	auto pThreadRunning = mpImpl->mThreadRunning;
	auto pService = mpImpl->mpIoService;

	mpImpl->mpSenderThread = UtilCRT::startThread(
		"UdpSender", 
		[pThreadRunning, pService]()
	{
		while (*pThreadRunning)
		{
			pService->run();
		}
	});

	return true;
}

CybertronCoreAPI void SocketUdpClient::clear()
{
	if (mpImpl->mThreadRunning != nullptr)
	{
		(*mpImpl->mThreadRunning) = false;

		if (mpImpl->mpIoService != nullptr)
		{
			mpImpl->mpIoService->stop();
		}
	}

	if (mpImpl->mpSenderThread != nullptr)
	{
		mpImpl->mpSenderThread->join();
	}

	mpImpl->mpSenderThread = nullptr;
	mpImpl->mpSocket = nullptr;
	mpImpl->mThreadRunning = nullptr;
	mpImpl->mpIoService = nullptr;
}

void encode(
	const void* pData,
	size_t len,
	std::string& encoded)
{
	std::stringstream rawInputStream;
	std::stringstream b64Stream;

	std::string s;
	s.insert(s.begin(), (const char*)pData, (const char*)pData + len);
	rawInputStream << s;

	base64::encoder E;
	E.encode(rawInputStream, b64Stream);

	std::string b64 = b64Stream.str();
	std::uint32_t crc = UtilCRC::crc32_encode(b64.c_str(), (size_t)b64.size());

	encoded.clear();
	for (size_t i = 0; i < 4; ++i)
	{
		encoded.push_back(1);
	}
	for (size_t i = 0; i < 4; ++i)
	{
		encoded.push_back(((char*)(&crc))[i]);
	}
	encoded.insert(
		encoded.end(),
		b64.c_str(),
		b64.c_str() + b64.size());
	for (size_t i = 0; i < 4; ++i)
	{
		encoded.push_back(0);
	}
}

CybertronCoreAPI void SocketUdpClient::broadcast(
	int port, 
	const void* pData, 
	size_t len)
{
	if (mpImpl->mpSocket == nullptr)
	{
		return;
	}

	if (!mpImpl->mConfig.isBroadcast)
	{
		return;
	}

	std::string encodedMessage;
	encode(pData, len, encodedMessage);

	auto end_point = asio::ip::udp::endpoint(asio::ip::address_v4::broadcast(), port);

	mpImpl->mpSocket->async_send_to(
		asio::buffer(encodedMessage.c_str(), encodedMessage.size()),
		end_point,
		[](asio::error_code ec, std::size_t bytes_transferred)
	{
		if (!ec) {
			IoPerf::inst().incr_send_packs(1);
			IoPerf::inst().incr_send_bytes(bytes_transferred);
		}
	});
}

CybertronCoreAPI void SocketUdpClient::send(
	const std::string& ip,
	int port,
	const void* pData,
	size_t len)
{
	if (mpImpl->mpSocket == nullptr)
	{
		return;
	}

	if (mpImpl->mConfig.isBroadcast)
	{
		return;
	}

	std::string encodedMessage;
	encode(pData, len, encodedMessage);

	auto end_point = asio::ip::udp::endpoint(asio::ip::address_v4::from_string(ip), port);

	mpImpl->mpSocket->async_send_to(
		asio::buffer(encodedMessage.c_str(), encodedMessage.size()),
		end_point,
		[](asio::error_code ec, std::size_t bytes_transferred)
	{
		if (!ec) {
			IoPerf::inst().incr_send_packs(1);
			IoPerf::inst().incr_send_bytes(bytes_transferred);
		}
	});
}

////////////////////////////////////////////////////

CybertronCoreAPI SocketUdpRawDataClient::SocketUdpRawDataClient()
{
    mpImpl = new SocketUdpClientImpl;
}

CybertronCoreAPI SocketUdpRawDataClient::~SocketUdpRawDataClient()
{
    delete mpImpl;
    mpImpl = nullptr;
}

CybertronCoreAPI bool SocketUdpRawDataClient::initialize(const SocketUdpSenderConfig& config)
{
    clear();

    mpImpl->mConfig = config;
    mpImpl->mThreadRunning = std::make_shared<bool>(true);
    mpImpl->mpIoService = std::make_shared<asio::io_service>();
    mpImpl->mpSocket = std::make_shared<asio::ip::udp::socket>(
        *(mpImpl->mpIoService),
        asio::ip::udp::endpoint(asio::ip::udp::v4(), 0));

    if (config.isBroadcast)
    {
        mpImpl->mpSocket->set_option(asio::socket_base::broadcast(true));
    }

    // start background thread
    auto pThreadRunning = mpImpl->mThreadRunning;
    auto pService = mpImpl->mpIoService;

    mpImpl->mpSenderThread = UtilCRT::startThread(
        "UdpSender",
        [pThreadRunning, pService]()
    {
        while (*pThreadRunning)
        {
            pService->run();
        }
    });

    return true;
}

CybertronCoreAPI void SocketUdpRawDataClient::clear()
{
    if (mpImpl->mThreadRunning != nullptr)
    {
        (*mpImpl->mThreadRunning) = false;

        if (mpImpl->mpIoService != nullptr)
        {
            mpImpl->mpIoService->stop();
        }
    }

    if (mpImpl->mpSenderThread != nullptr)
    {
        mpImpl->mpSenderThread->join();
    }

    mpImpl->mpSenderThread = nullptr;
    mpImpl->mpSocket = nullptr;
    mpImpl->mThreadRunning = nullptr;
    mpImpl->mpIoService = nullptr;
}

CybertronCoreAPI void SocketUdpRawDataClient::broadcast(
    int port,
    const void* pData,
    size_t len)
{
    if (mpImpl->mpSocket == nullptr)
    {
        return;
    }

    if (!mpImpl->mConfig.isBroadcast)
    {
        return;
    }

    auto end_point = asio::ip::udp::endpoint(asio::ip::address_v4::broadcast(), port);

    mpImpl->mpSocket->async_send_to(
        asio::buffer(pData, len),
        end_point,
        [](asio::error_code ec, std::size_t bytes_transferred)
    {
        if (!ec) {
            IoPerf::inst().incr_send_packs(1);
            IoPerf::inst().incr_send_bytes(bytes_transferred);
        }
    });
}

CybertronCoreAPI void SocketUdpRawDataClient::send(
    const std::string& ip,
    int port,
    const void* pData,
    size_t len)
{
    if (mpImpl->mpSocket == nullptr)
    {
        return;
    }

    if (mpImpl->mConfig.isBroadcast)
    {
        return;
    }

    auto end_point = asio::ip::udp::endpoint(asio::ip::address_v4::from_string(ip), port);

    mpImpl->mpSocket->async_send_to(
        asio::buffer(pData, len),
        end_point,
        [](asio::error_code ec, std::size_t bytes_transferred)
    {
        if (!ec) {
            IoPerf::inst().incr_send_packs(1);
            IoPerf::inst().incr_send_bytes(bytes_transferred);
        }
    });
}

CYBERTRON_END
